package io.noties.markwon.image;


import com.noties.markwon.annotation.NonNull;
import com.noties.markwon.annotation.Nullable;
import com.noties.markwon.annotation.VisibleForTesting;
import com.noties.markwon.utils.LogUtils;
import com.noties.markwon.wrapper.graphics.drawable.Drawable;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import ohos.agp.utils.Rect;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.miscservices.timeutility.Time;
import ohos.utils.net.Uri;

class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {

    private final ExecutorService executorService;
    private final Map<String, SchemeHandler> schemeHandlers;
    private final Map<String, MediaDecoder> mediaDecoders;
    private final MediaDecoder defaultMediaDecoder;
    private final ImagesPlugin.PlaceholderProvider placeholderProvider;
    private final ImagesPlugin.ErrorHandler errorHandler;

    private final EventHandler handler;
    private Runnable task;
    // @since 4.0.0 use a hash-map with a AsyncDrawable as key for multiple requests
    //  for the same destination
    private final Map<AsyncDrawable, Future<?>> requests = new HashMap<>(2);

    AsyncDrawableLoaderImpl(@NonNull AsyncDrawableLoaderBuilder builder) {
        this(builder, new EventHandler(EventRunner.getMainEventRunner()));
    }

    // @since 4.0.0
    @VisibleForTesting
    private AsyncDrawableLoaderImpl(@NonNull AsyncDrawableLoaderBuilder builder, @NonNull EventHandler handler) {
        this.executorService = builder.executorService;
        this.schemeHandlers = builder.schemeHandlers;
        this.mediaDecoders = builder.mediaDecoders;
        this.defaultMediaDecoder = builder.defaultMediaDecoder;
        this.placeholderProvider = builder.placeholderProvider;
        this.errorHandler = builder.errorHandler;
        this.handler = handler;
    }

    @Override
    public void load(@NonNull final AsyncDrawable drawable) {
        final Future<?> future = requests.get(drawable);
        if (future == null) {
            requests.put(drawable, execute(drawable));
        }
    }

    @Override
    public void cancel(@NonNull final AsyncDrawable drawable) {

        final Future<?> future = requests.remove(drawable);
        if (future != null) {
            future.cancel(true);
        }

        handler.removeTask(task, drawable);
    }

    @Nullable
    @Override
    public Drawable placeholder(@NonNull AsyncDrawable drawable) {
        return placeholderProvider != null
            ? placeholderProvider.providePlaceholder(drawable)
            : null;
    }

    @NonNull
    private Future<?> execute(@NonNull final AsyncDrawable asyncDrawable) {
        return executorService.submit(() -> {

            final String destination = asyncDrawable.getDestination();

            final Uri uri = Uri.parse(destination);

            Drawable drawable = null;

            try {

                final String scheme = uri.getScheme();
                if (scheme == null
                    || scheme.length() == 0) {
                    throw new IllegalStateException("No scheme is found: " + destination);
                }

                // obtain scheme handler
                final SchemeHandler schemeHandler = schemeHandlers.get(scheme);
                if (schemeHandler != null) {

                    // handle scheme
                    final ImageItem imageItem = schemeHandler.handle(destination, uri);

                    // if resulting imageItem needs further decoding -> proceed
                    if (imageItem.hasDecodingNeeded()) {

                        final ImageItem.WithDecodingNeeded withDecodingNeeded = imageItem.getAsWithDecodingNeeded();

                        // @since 4.6.2 close input stream
                        try {
                            MediaDecoder mediaDecoder = mediaDecoders.get(withDecodingNeeded.contentType());

                            if (mediaDecoder == null) {
                                mediaDecoder = defaultMediaDecoder;
                            }

                            if (mediaDecoder != null) {
                                drawable = mediaDecoder
                                    .decode(withDecodingNeeded.contentType(), withDecodingNeeded.inputStream());
                            } else {
                                // throw that no media decoder is found
                                throw new IllegalStateException("No media-decoder is found: " + destination);
                            }
                        } finally {
                            try {
                                withDecodingNeeded.inputStream().close();
                            } catch (IOException e) {
                                LogUtils.w(LogUtils.HI_LOG_LABEL, e.toString());

                            }
                        }
                    } else {
                        drawable = imageItem.getAsWithResult().result();
                    }
                } else {
                    // throw no scheme handler is available
                    throw new IllegalStateException("No scheme-handler is found: " + destination);
                }

            } catch (Throwable t) {
                if (errorHandler != null) {
                    drawable = errorHandler.handleError(destination, t);
                } else {
                    // else simply log the error
                    LogUtils.w(LogUtils.HI_LOG_LABEL, "Error loading image: " + destination);
                }
            }

            final Drawable out = drawable;

            // @since 4.0.0 apply intrinsic bounds (but only if they are empty)
            if (out != null) {
                final Rect bounds = out.getBounds();
                //noinspection ConstantConditions
                if (bounds == null
                    || bounds.isEmpty()) {
                    DrawableUtils.applyIntrinsicBounds(out);
                }
            }

            task = new Runnable() {
                @Override
                public void run() {
                    // validate that
                    // * request was not cancelled
                    // * out-result is present
                    // * async-drawable is attached
                    final Future<?> future = requests.remove(asyncDrawable);
                    if (future != null
                        && out != null
                        && asyncDrawable.isAttached()) {
                        asyncDrawable.setResult(out);
                    }
                }
            };
            handler.postTimingTask(task, asyncDrawable, Time.getRealActiveTime());
        });
    }
}
